Skip to content

Fix jarsigner PKIX error for non-exportable AKV certificates via AIA chain completion#47977

Open
rujche with Copilot wants to merge 45 commits into
mainfrom
copilot/fix-jarsigner-certificate-chain-issue
Open

Fix jarsigner PKIX error for non-exportable AKV certificates via AIA chain completion#47977
rujche with Copilot wants to merge 45 commits into
mainfrom
copilot/fix-jarsigner-certificate-chain-issue

Conversation

Copilot AI commented Feb 11, 2026

Copy link
Copy Markdown
Contributor

Problem

When a non-exportable DigiCert certificate in Azure Key Vault is used with jarsigner, the signed JAR fails verification with:

Warning:
This jar contains entries whose certificate chain is invalid. Reason:
PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException:
  unable to find valid certification path to requested target

Fixes #44267 (regression confirmed unfixed in 2.11.0-beta.1 per this comment).

Root Cause

The Azure Key Vault /secrets/{alias} endpoint returns only the leaf certificate for non-exportable keys (no private key, and no intermediate CA certificates when the caller only provided the leaf cert during MergeCertificate). The getCertificateChain() method therefore returns a one-element array. An earlier fix (PR #41303) added intermediate cert support and PR #47977 added ordering — neither helps when the intermediate is genuinely absent.

Without the intermediate CA in the chain:

  • jarsigner embeds only the leaf cert in the JAR signature block
  • jarsigner -verify calls Java's PKIX path builder to trace leaf -> intermediate -> root CA
  • The intermediate is not in the JAR and not in cacerts -> path building fails

Solution

After loading certificates from the AKV secret bundle, walk the chain upward. If the top certificate is not self-signed (i.e. the chain is incomplete), parse its AIA (Authority Information Access) extension (OID 1.3.6.1.5.5.7.1.1) to find the caIssuers URL, download the missing intermediate CA certificate (DER or PEM), and append it. Repeat until the chain reaches a self-signed root CA or no AIA URL is found.

This is the standard mechanism used by browsers, AzureSignTool, and major signing tools to complete certificate chains at runtime.

Example (AIA in certificate context):

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            12:34:56:78:9a:bc
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, O=Example PKI, CN=Example Intermediate CA
        Validity
            Not Before: Jul  1 00:00:00 2026 GMT
            Not After : Jul  1 23:59:59 2028 GMT
        Subject: C=US, O=Example Corp, CN=example-signer

        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Key Usage: critical
                Digital Signature
            Authority Information Access:
                OCSP - URI:http://ocsp.example.com
                CA Issuers - URI:http://cacerts.example.com/ExampleIntermediateCA.crt
            X509v3 Subject Key Identifier:
                4A:7F:01:2B:...

In this flow, chain completion follows CA Issuers level by level (leaf -> intermediate -> root) until the chain is complete or no issuer can be resolved.

Implementation

Core Algorithm

  • Chain Completion: Standard AIA-based chain completion per RFC 4158.
  • Leaf Selection: Prioritizes certificates with existing issuers in chain; falls back to first non-issuer (order-independent). Self-signed roots are only used as fallback in incomplete chains.
  • Chain End Detection: Uses findValidChainEnd() to identify true chain end (prevents interfering with extra/unplaced certs).
  • Issuer Selection from AIA Payloads: When AIA responds with multiple certs (bundle), selects the cert that matches expected issuer DN and can verify the current cert signature.
  • Duplicate/Existing Issuer Handling: Detects valid issuers already present in chain and repositions to contiguous order when needed.
  • Safety Limits: Max 10 issuer-resolution attempts per chain to prevent infinite loops.

Security Hardening

  • SSRF Protection: System property azure.keyvault.jca.disable-aia-download allows disabling AIA in locked-down environments.
  • Self-Signed Verification: Validates both subject==issuer and signature verification.
  • Issuer Validation:
    • Signature must verify with issuer public key.
    • Issuer must be a CA (basicConstraints or self-signed root).
    • If KeyUsage is present, keyCertSign must be set (RFC 5280 alignment).
  • Robust Fallback Parsing: Supports DER and PEM AIA responses and validates issuer before chain insertion.

Lint and Review Follow-ups

  • Narrowed exception type in isValidIssuer to satisfy SpotBugs (REC_CATCH_EXCEPTION).
  • Updated long log messages to satisfy Checkstyle line-length limits.
  • Aligned chain diagnostic logging (Self-Signed) with signature-based self-signed verification.
  • Corrected findValidChainEnd JavaDoc wording to match actual chain-walking direction.

Changes

File Change
CertificateUtil.java Add completeChainViaAia(), downloadIssuerCertificateFromAia(), findValidChainEnd(), isSelfSignedCertificate(), isValidIssuer(); improve orderCertificateChain(); enforce KeyUsage keyCertSign; select matching issuer from AIA bundles; improve diagnostics and lint compliance
HttpUtil.java Add getBytes(String url) for binary downloads (10s timeout) with exception logging for diagnostics
AiaCertificateChainTest.java Expanded AIA coverage to 13 tests including PEM bundle issuer selection and KeyUsage edge case
CertificateOrderTest.java 5 tests verifying certificate chain ordering (leaf -> intermediate -> root) for PEM and PKCS12 formats, including regression test for incomplete [root, leaf] chains and concrete single-cert edge case
CHANGELOG.md Document bug fix, system property, and security advisory
README.md Add azure.keyvault.jca.disable-aia-download to Exposed Options
checkstyle-suppressions.xml Keep module-level GoodLoggingCheck suppression for CertificateUtil.java/HttpUtil.java (module continues to use java.util.logging)

Test Coverage

PKIX Path Building:

Certificate Ordering (PEM/PKCS12):

  • testPemCertificateChainOrder ✓ Verifies correct leaf -> intermediate -> root ordering
  • testPkcs12CertificateChainOrder ✓ Validates PKCS12 format chain ordering
  • testOrderCertificateChainIncompleteRootFirst ✓ Regression test: [root, leaf] input correctly orders to [leaf, root]

AIA Chain Completion:

  • Leaf-only -> downloads intermediate and root ✓
  • Full chain -> skips unnecessary downloads ✓
  • Cross-signed certificates -> handled correctly ✓
  • Duplicate detection/repositioning -> preserves contiguous valid chain ✓
  • PEM bundle AIA response -> selects matching issuer cert, not first entry ✓
  • KeyUsage CA edge case -> rejects issuer missing keyCertSign when KeyUsage present ✓
  • System property disable -> azure.keyvault.jca.disable-aia-download=true prevents AIA downloads ✓

Results: 97/97 tests pass (0 failures, 29 skipped)

Customer Validation

Confirmed working by the original reporter (@manfrede) in #44267 (comment) using azure-security-keyvault-jca-2.12.0-beta.1.jar. The AIA chain completion path executed correctly as shown in debug logs:

FEIN: Downloading issuer certificate from AIA URL: http://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt
FEIN: Downloaded intermediate CA certificate via AIA: CN=DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1,...
FEIN: Downloading issuer certificate from AIA URL: http://cacerts.digicert.com/DigiCertTrustedRootG4.crt
FEIN: Downloaded intermediate CA certificate via AIA: CN=DigiCert Trusted Root G4,...
FEIN: Certificate chain is complete. Root CA: CN=DigiCert Trusted Root G4,...
FEIN: Certificate chain after AIA completion [3 certs]:
  [0] leaf (CN=Industrie Informatik GmbH)
  [1] CN=DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1 | Self-Signed: false
  [2] CN=DigiCert Trusted Root G4 | Self-Signed: true

jarsigner produced no warnings and the full chain was embedded correctly.

Backward Compatibility

  • ✓ Existing behavior preserved for complete chains
  • ✓ AIA completion enabled by default
  • ✓ System property opt-out for locked-down environments
  • ✓ No breaking changes to public APIs

Copilot AI and others added 4 commits February 11, 2026 02:26
Co-authored-by: rujche <171773178+rujche@users.noreply.github.com>
Co-authored-by: rujche <171773178+rujche@users.noreply.github.com>
Co-authored-by: rujche <171773178+rujche@users.noreply.github.com>
Co-authored-by: rujche <171773178+rujche@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix jarsigner warning for invalid certificate chain Fix certificate chain ordering for jarsigner compatibility Feb 11, 2026
Copilot AI requested a review from rujche February 11, 2026 02:36
@rujche rujche added azure-spring All azure-spring related issues azure-spring-jca labels Feb 11, 2026
@rujche rujche added this to the 2026-03 milestone Feb 11, 2026
@rujche rujche moved this from Todo to In Progress in Spring Cloud Azure Feb 11, 2026
@rujche rujche moved this from In Progress to Need Feedback in Spring Cloud Azure Feb 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Hi @copilot. Thank you for your interest in helping to improve the Azure SDK experience and for your contribution. We've noticed that there hasn't been recent engagement on this pull request. If this is still an active work stream, please let us know by pushing some changes or leaving a comment. Otherwise, we'll close this out in 7 days.

@rujche rujche modified the milestones: 2026-03, Backlog May 8, 2026
rujche added 2 commits July 8, 2026 10:23
…ficates

When a non-exportable Azure Key Vault certificate is used with jarsigner,
the /secrets/ endpoint returns only the leaf certificate (no intermediate
CAs). This causes jarsigner -verify to fail with:

  PKIX path building failed: unable to find valid certification path
  to requested target

Fix: after loading certificates from the AKV secret bundle, walk the
chain upward from the current top. If the top cert is not self-signed
(i.e. the chain is incomplete), parse the AIA (Authority Information
Access) extension (OID 1.3.6.1.5.5.7.1.1) to find the CA Issuers URL
and download the missing intermediate CA certificate. Repeat until the
chain reaches a self-signed root CA.

Changes:
- CertificateUtil: add completeChainViaAia() and
  downloadIssuerCertificateFromAia() methods; call completeChainViaAia()
  from loadCertificatesFromSecretBundleValue() after ordering
- HttpUtil: add getBytes(String url) for binary (DER) certificate downloads
- AiaCertificateChainTest: 10 unit tests including two PKIX path-building
  tests that reproduce the exact reported error and confirm it is resolved

Fixes: #44267
@rujche rujche changed the title Fix certificate chain ordering for jarsigner compatibility Fix jarsigner PKIX error for non-exportable AKV certificates via AIA chain completion Jul 8, 2026
@rujche rujche requested a review from Copilot July 8, 2026 02:54
@rujche rujche moved this from Need Feedback to In Progress in Spring Cloud Azure Jul 8, 2026
@rujche rujche removed this from the Backlog milestone Jul 8, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

- Fix maxDownloads counter to only decrement on actual HTTP downloads, not during certificate reordering. This prevents premature loop exit when reorganizing existing issuer certificates.
- Replace string concatenation with parameterized logging in HttpUtil.getBytes() to satisfy GoodLoggingCheck.
- Restore GoodLoggingCheck suppressions for CertificateUtil.java and HttpUtil.java (module requires java.util.logging for JCA provider bootstrap).

Addresses: maxDownloads safety limit, GoodLoggingCheck violation in exception logging

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

@rujche rujche requested a review from Copilot July 8, 2026 09:36
@rujche rujche marked this pull request as ready for review July 8, 2026 09:38
@rujche rujche requested review from a team as code owners July 8, 2026 09:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

@rujche rujche requested a review from Copilot July 9, 2026 01:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

@vcolin7 vcolin7 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes look good for an initial review, but there are a few places where I think we can make improvements. If you have any questions, I'd be happy to discuss my comments further :)

// Complete the chain by downloading any missing intermediate CA certificates via the AIA extension.
// This handles the case where only the leaf certificate was stored in Azure Key Vault
// (e.g. a non-exportable certificate where the caller only merged the leaf cert during CSR completion).
certificates = completeChainViaAia(certificates);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way the code is written, this will run on every KeyVaultClient.getCertificateChain() call, not just when jarsigner is used (like when using TLS in Spring Boot / App Service); meaning that completeChainViaAia() will now issue an outbound HTTP call on every certificate load and on every refresh interval, turning a previously offline, in-memory operation into a network-dependent one by default (up to 10s per missing link).

The self-signed short-circuit and having an opt-out property mitigate this, but enabling it by default for the entire TLS path is a meaningful behavioral change. @rujche Can we confirm this is an intentional decision?

Two alternatives worth considering: (a) only complete the chain when the loaded chain has a single cert (the true leaf-only, non-exportable case that #44267 describes), or (b) make completion opt-in for the TLS path and keep it default-on only where signing chains are needed.

* @param orderedCertificates certificate array with contiguous issuer path + any unplaced certs appended
* @return the (potentially extended) certificate array with missing intermediates inserted in the valid chain
*/
static Certificate[] completeChainViaAia(Certificate[] orderedCertificates) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now, completeChainViaAia() re-downloads issuer certs from the CA's AIA endpoint on every incomplete chain load, and again on every certificates-refresh-interval cycle, for every alias that shares an issuer. Since issuer/intermediate/root certs are effectively immutable, we'd have repeated/avoidable latency and a runtime dependency on public CA endpoints (e.g. cacerts.digicert.com), which can become noticeable at scale (many instances/aliases x refresh cycles hitting the same few URLs with their own throttling rules/rate-limits).

I think it's worth considering using a small static in-memory cache using the CA Issuers URL (or issuer subject DN) as keys to eliminate said repeated roundtrips. If you'd prefer to keep this PR focused on the core fix, we can do so in a separate PR.

To keep the cache safe, I'd recommend the following:

  1. Caching the bytes, not the validation. We'd only store the parsed/validated issuer cert and still run isValidIssuer(...) (signature + CA + keyCertSign) against the current certificate on every use whether it's in the cache or not. This way, we'd skip the HTTP GET on a hit, but never the crypto validation. Otherwise, a malicious leaf pointing at a URL with a cached-but-wrong issuer could shortcut verification.

  2. Limiting the cache size. We'd use a size cap (like a bounded ConcurrentHashMap or similar) and ideally a TTL, so a cert advertising many distinct AIA URLs can't grow the map without limit (memory-pressure DoS) and a reissued/revoked intermediate isn't served stale indefinitely.

Comment on lines +389 to +402
if (validIssuerInChain != null) {
// Issuer exists in the chain. If it's not in the expected position (validChainEnd+1),
// move it to make the chain contiguous
if (validIssuerIndex != validChainEnd + 1) {
LOGGER.log(FINE, "Valid issuer found but not at contiguous position. Moving from index {0} to {1}.",
new Object[] { validIssuerIndex, validChainEnd + 1 });
chain.remove(validIssuerIndex);
chain.add(validChainEnd + 1, validIssuerInChain);
} else {
LOGGER.log(FINE, "Valid issuer already at correct contiguous position.");
}
// Continue the loop to potentially download the issuer's issuer
continue;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The maxDownloads safety counter is only decremented when downloading (on line 406), but the branch that repositions a valid issuer already present in the chain ends in continue without decrementing the count, so loop termination there depends entirely on findValidChainEnd() advancing on every iteration. It seems the normal cases should terminate, but a malformed or adversarial input set (e.g. having the same intermediate issue twice in the chain, or a re-issued / cross-signed intermediate that shares subject DN + key) could theoretically keep the loop spinning.

I'd recommend adding a hard iteration cap on the outer while (true) (independent of maxDownloads) as a defensive guard, so termination doesn't rely on the reposition logic always making forward progress.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can refer to this suggestion.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This PR adds 500+ lines of security-sensitive PKI logic (chain ordering, AIA resolution, issuer validation, self-signed detection) to a single util class as part of a bugfix. The code is well-structured and thoroughly tested, so I wouldn't consider my comment a blocker, but it's worth pointing out there's a substantial amount of new crypto surface to own and maintain. It's worth considering whether the AIA-completion logic belongs in its own dedicated class (e.g. AiaChainCompleter) to keep CertificateUtil focused and make the security-critical path easier to review and test in isolation.

private static final Logger LOGGER = Logger.getLogger(CertificateUtil.class.getName());
private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----";
private static final String END_CERTIFICATE = "-----END CERTIFICATE-----";
static final String DISABLE_AIA_DOWNLOAD_PROPERTY = "azure.keyvault.jca.disable-aia-download";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a short Javadoc note here explaining what this property does like we did in the CHANGELOG/README? It might help maintainers in the future :)

Comment on lines +615 to +618
} catch (GeneralSecurityException e) {
// If signature verification fails or any error occurs, it's not a valid issuer
return false;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception handling:

Suggested change
} catch (GeneralSecurityException e) {
// If signature verification fails or any error occurs, it's not a valid issuer
return false;
}
} catch (CertificateExpiredException | CertificateNotYetValidException e) {
LOGGER.log(FINE, "Issuer certificate [{0}] is expired or not yet valid; rejecting it as an issuer.",
issuer.getSubjectX500Principal().getName());
return false;
} catch (GeneralSecurityException e) {
// If signature verification fails or any other error occurs, it's not a valid issuer
return false;
}

You'd also need to add the following import statements:

import java.security.cert.CertificateExpiredException;
...
import java.security.cert.CertificateNotYetValidException;

assertEquals(leafWithBadIssuerAia, result[0]);
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add a test where we build an issuer that is a valid CA in every respect EXCEPT that it has already expired for isValidIssuer() to reject via checkValidity().

Suggested change
@Test
void completeChainViaAiaRejectsExpiredIssuer() throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
// Build an issuer that is a valid CA in every respect EXCEPT that it has already expired.
// isValidIssuer must reject it via checkValidity() so it is never inserted into the chain.
Date expiredNotBefore = new Date(System.currentTimeMillis() - 86_400_000L * 30);
Date expiredNotAfter = new Date(System.currentTimeMillis() - 86_400_000L);
KeyPair expiredIssuerKeyPair = keyGen.generateKeyPair();
X509Certificate expiredIssuerCert
= buildCertificate(expiredIssuerKeyPair.getPublic(), "CN=Expired Issuer", "CN=Expired Issuer",
expiredIssuerKeyPair.getPrivate(), true, null, KeyUsage.keyCertSign, expiredNotBefore, expiredNotAfter);
KeyPair leafKeyPair = keyGen.generateKeyPair();
X509Certificate leafWithExpiredAia = buildCertificate(leafKeyPair.getPublic(), "CN=Leaf", "CN=Expired Issuer",
expiredIssuerKeyPair.getPrivate(), false, AIA_BAD_ISSUER_URL);
try (MockedStatic<HttpUtil> httpMock = Mockito.mockStatic(HttpUtil.class)) {
httpMock.when(() -> HttpUtil.getBytes(AIA_BAD_ISSUER_URL)).thenReturn(expiredIssuerCert.getEncoded());
Certificate[] result = CertificateUtil.completeChainViaAia(new Certificate[] { leafWithExpiredAia });
assertEquals(1, result.length,
"An expired issuer certificate must be rejected and not inserted into the chain");
assertEquals(leafWithExpiredAia, result[0]);
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test would need this change.

Comment on lines +391 to +397
private static X509Certificate buildCertificate(java.security.PublicKey subjectPublicKey, String subjectDn,
String issuerDn, PrivateKey signingKey, boolean isCa, String aiaUrl, Integer keyUsageFlags) throws Exception {

X500Name subject = new X500Name(subjectDn);
X500Name issuer = new X500Name(issuerDn);
Date notBefore = new Date(System.currentTimeMillis() - 86_400_000L);
Date notAfter = new Date(System.currentTimeMillis() + 86_400_000L * 365);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding overload to use in the proposed new test.

Suggested change
private static X509Certificate buildCertificate(java.security.PublicKey subjectPublicKey, String subjectDn,
String issuerDn, PrivateKey signingKey, boolean isCa, String aiaUrl, Integer keyUsageFlags) throws Exception {
X500Name subject = new X500Name(subjectDn);
X500Name issuer = new X500Name(issuerDn);
Date notBefore = new Date(System.currentTimeMillis() - 86_400_000L);
Date notAfter = new Date(System.currentTimeMillis() + 86_400_000L * 365);
private static X509Certificate buildCertificate(java.security.PublicKey subjectPublicKey, String subjectDn,
String issuerDn, PrivateKey signingKey, boolean isCa, String aiaUrl, Integer keyUsageFlags) throws Exception {
Date notBefore = new Date(System.currentTimeMillis() - 86_400_000L);
Date notAfter = new Date(System.currentTimeMillis() + 86_400_000L * 365);
return buildCertificate(subjectPublicKey, subjectDn, issuerDn, signingKey, isCa, aiaUrl, keyUsageFlags,
notBefore, notAfter);
}
private static X509Certificate buildCertificate(java.security.PublicKey subjectPublicKey, String subjectDn,
String issuerDn, PrivateKey signingKey, boolean isCa, String aiaUrl, Integer keyUsageFlags, Date notBefore,
Date notAfter) throws Exception {
X500Name subject = new X500Name(subjectDn);
X500Name issuer = new X500Name(issuerDn);

List<Certificate> chain = new ArrayList<>(Arrays.asList(orderedCertificates));
int maxDownloads = 10; // Safety limit to prevent infinite loops

while (true) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
while (true) {
// Hard limit on outer iterations, independent of maxDownloads. Repositioning below can take
// `continue` without decrementing maxDownloads, so overall termination must not rely solely on
// findValidChainEnd() moving the index forward. Malformed or adversarial input (e.g. duplicate or
// cross-signed intermediate issuers sharing a subject DN and key) could otherwise keep the reposition
// logic looping indefinitely. The bound is intentionally generous so it never truncates a legitimately
// completable chain.
int remainingIterations = 4 * (chain.size() + maxDownloads) + 16;
while (true) {
if (--remainingIterations < 0) {
LOGGER.log(FINE, "Reached maximum certificate chain-completion iterations. Stopping to guard against "
+ "non-terminating input (possible duplicate or cross-signed intermediates).");
break;
}

Comment on lines +389 to +402
if (validIssuerInChain != null) {
// Issuer exists in the chain. If it's not in the expected position (validChainEnd+1),
// move it to make the chain contiguous
if (validIssuerIndex != validChainEnd + 1) {
LOGGER.log(FINE, "Valid issuer found but not at contiguous position. Moving from index {0} to {1}.",
new Object[] { validIssuerIndex, validChainEnd + 1 });
chain.remove(validIssuerIndex);
chain.add(validChainEnd + 1, validIssuerInChain);
} else {
LOGGER.log(FINE, "Valid issuer already at correct contiguous position.");
}
// Continue the loop to potentially download the issuer's issuer
continue;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another guard to avoid keeping the loop going on indefinitely.

Suggested change
if (validIssuerInChain != null) {
// Issuer exists in the chain. If it's not in the expected position (validChainEnd+1),
// move it to make the chain contiguous
if (validIssuerIndex != validChainEnd + 1) {
LOGGER.log(FINE, "Valid issuer found but not at contiguous position. Moving from index {0} to {1}.",
new Object[] { validIssuerIndex, validChainEnd + 1 });
chain.remove(validIssuerIndex);
chain.add(validChainEnd + 1, validIssuerInChain);
} else {
LOGGER.log(FINE, "Valid issuer already at correct contiguous position.");
}
// Continue the loop to potentially download the issuer's issuer
continue;
}
if (validIssuerInChain != null) {
if (validIssuerIndex > validChainEnd + 1) {
// Valid issuer sits among the appended/unplaced certs after the valid prefix.
// Moving it up into the contiguous slot is safe: the removal is beyond the valid
// prefix, so it cannot break an earlier link. This makes forward progress, so
// re-evaluate the chain from the top.
LOGGER.log(FINE, "Valid issuer found but not at contiguous position. Moving from index {0} to {1}.",
new Object[] { validIssuerIndex, validChainEnd + 1 });
chain.remove(validIssuerIndex);
chain.add(validChainEnd + 1, validIssuerInChain);
continue;
} else if (validIssuerIndex <= validChainEnd) {
// The matching issuer lies *inside* the already-valid prefix. This can occur with
// duplicate or cross-signed intermediates that share a subject DN and key. Removing
// it would break the valid prefix and could keep the loop oscillating between two
// chain arrangements without ever decrementing maxDownloads, so do NOT reposition here.
// Fall through to the bounded AIA download branch, which inserts a fresh issuer copy
// contiguously and makes guaranteed forward progress.
LOGGER.log(FINE,
"Valid issuer for [{0}] found inside the valid prefix at index {1} (likely duplicate or "
+ "cross-signed). Not repositioning; attempting AIA download instead.",
new Object[] { x509Top.getSubjectX500Principal().getName(), validIssuerIndex });
} else {
// validIssuerIndex == validChainEnd + 1: already contiguous. findValidChainEnd would
// normally have consumed it already; fall through rather than spinning on `continue`.
LOGGER.log(FINE, "Valid issuer already at correct contiguous position.");
}
// Fall through to the AIA download branch below.
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

azure-spring All azure-spring related issues azure-spring-jca

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

[BUG] jarsigner + jca still reports that entries in certificate chain are invalid

5 participants